﻿Imports Kairyu.DCEngine
Imports System.Text
Imports System.Reflection

''' <summary>
''' モデルをDBに保存します
''' </summary>
''' <remarks></remarks>
Public Class Saver
    Inherits Loader

    Public Shared Function Create(trn As IDbTransaction, prefix As String) As Saver
        If trn Is Nothing Then Throw New ArgumentNullException("trn")
        If trn Is Nothing Then Throw New ArgumentNullException("prefix")

        Dim c As New Saver(trn.Connection, trn, prefix)
        Return c
    End Function

    Private Sub New(cn As IDbConnection, trn As IDbTransaction, prefix As String)
        MyBase.New(cn, prefix)
        Me.Transaction = trn
    End Sub

    Private _transaction As IDbTransaction
    ''' <summary>
    ''' トランザクション
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Property Transaction As IDbTransaction
        Get
            Return _transaction
        End Get
        Private Set(value As IDbTransaction)
            _transaction = value
        End Set
    End Property

    ''' <summary>
    ''' DBコマンドを作成します
    ''' </summary>
    ''' <param name="text"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Overrides Function CreateCommand(text As String) As System.Data.IDbCommand
        Dim cmd = MyBase.CreateCommand(text)
        cmd.Transaction = Me.Transaction
        Return cmd
    End Function

    ''' <summary>
    ''' 追加します
    ''' </summary>
    ''' <param name="instance"></param>
    ''' <remarks></remarks>
    Public Sub SaveAsNew(instance As Object)
        Dim ds = Storage.ToAddDirtyObjects(instance)
        Me.Save(ds)
    End Sub

    ''' <summary>
    ''' 削除します
    ''' </summary>
    ''' <param name="instance"></param>
    ''' <remarks></remarks>
    Public Sub SaveAsDelete(instance As Object)
        Dim ds = Storage.ToDeleteDirtyObjects(instance)
        Me.Save(ds)
    End Sub

    ''' <summary>
    ''' 保存します
    ''' </summary>
    ''' <param name="storage"></param>
    ''' <param name="instance"></param>
    ''' <remarks></remarks>
    Public Sub Save(storage As Storage, instance As Object)
        If storage Is Nothing Then Throw New ArgumentNullException("storage")

        Me.Save(storage.ToDirtyObjects(instance))
    End Sub

    ''' <summary>
    ''' 保存します
    ''' </summary>
    ''' <param name="ds"></param>
    ''' <remarks></remarks>
    Public Sub Save(ds As IEnumerable(Of DirtyObject))
        If ds Is Nothing Then Throw New ArgumentNullException("ds")

        'TODO シーケンスのキャッシュ

        '処理
        For Each item In ds
            Dim addDO As AddDirtyObject = Nothing
            Dim cmd As IDbCommand = Nothing

            Select Case item.DirtyType
                Case DirtyTypes.Add
                    addDO = DirectCast(item, AddDirtyObject)

                    '事前処理（カスタムキー）
                    Me.SetCustomKey(addDO)

                    cmd = Me.BuildInsertCommand(addDO)
                Case DirtyTypes.Update
                    cmd = Me.BuildUpdateCommand(DirectCast(item, UpdateDirtyObject))
                Case DirtyTypes.Delete
                    cmd = Me.BuildDeleteCommand(DirectCast(item, DeleteDirtyObject))
            End Select

            Dim cnt = cmd.Logging.ExecuteNonQuery
            If cnt = 0 Then
                Throw New InvalidOperationException("他のユーザが更新しているため、保存に失敗しました。")
            ElseIf cnt > 1 Then
                'ログ
            End If

            If addDO Is Nothing Then Continue For

            '事後処理（オートナンバー）
            Me.SetAutoNumberKey(addDO)
        Next
    End Sub

    ''' <summary>
    ''' カスタムキーを代入します
    ''' </summary>
    ''' <param name="c"></param>
    ''' <remarks></remarks>
    Private Sub SetCustomKey(c As AddDirtyObject)
        For Each prop In c.KeyProperties
            Dim ckey = prop.ToCommandKey
            If ckey Is Nothing OrElse ckey.KeyType <> KeyTypes.Custom Then Continue For
            Dim val = Me.CreateCommand(ckey.CommandText).Logging.ExecuteScalar
            prop.SetLiteralValue(c.Instance, val)
        Next
    End Sub

    ''' <summary>
    ''' オートナンバーを代入します
    ''' </summary>
    ''' <param name="c"></param>
    ''' <remarks></remarks>
    Private Sub SetAutoNumberKey(c As AddDirtyObject)
        For Each prop In c.KeyProperties
            Dim ckey = prop.ToCommandKey
            If ckey Is Nothing OrElse ckey.KeyType <> KeyTypes.AutoNumber Then Continue For
            Dim val = Me.CreateCommand(ckey.CommandText).Logging.ExecuteScalar
            prop.SetLiteralValue(c.Instance, val)
        Next
    End Sub

#Region "Build DbCommand"
    ''' <summary>
    ''' インサートクエリを構築します
    ''' </summary>
    ''' <param name="c"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Private Function BuildInsertCommand(c As AddDirtyObject) As IDbCommand
        Dim cmd = Me.CreateCommand

        Dim csql As New StringBuilder
        Dim vsql As New StringBuilder
        Dim vcnt As Integer = 0

        '列、値文字列
        For Each item In c.DirtyProperties
            'オートナンバー例外
            Dim ck = item.ToCommandKey
            If ck IsNot Nothing AndAlso ck.KeyType = KeyTypes.AutoNumber Then Continue For

            Dim cname = item.LiteralName

            'ハードコーディング処理
            If item.HasSystemValue AndAlso item.SystemValueAttribute.IsHardCode Then
                csql.AppendDelimiter(", ").Append(cname)
                vsql.AppendDelimiter(", ").Append(item.SystemValueAttribute.HardCode)
                Continue For
            End If

            Dim cval = item.GetLiteralValueForInsert(c.Instance)
            Dim pname = Me.ParameterPrefix & "V" & vcnt

            '列
            csql.AppendDelimiter(", ").Append(cname)

            '値
            Dim prm = cmd.AddParameter(pname, cval)
            vsql.AppendDelimiter(", ").Append(prm.ParameterName)

            vcnt += 1
        Next

        '文字列結合
        Dim tname = c.InstanceType.ToEntityDefinition.EntityName
        cmd.CommandText = String.Format("INSERT INTO {0} ({1}) VALUES ({2})", tname, csql.ToString, vsql.ToString)

        Return cmd
    End Function

    ''' <summary>
    ''' アップデートクエリを構築します
    ''' </summary>
    ''' <param name="c"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Private Function BuildUpdateCommand(c As UpdateDirtyObject) As IDbCommand
        Dim cmd = Me.CreateCommand

        Dim vsql As New StringBuilder
        Dim vcnt As Integer = 0

        '列文字列
        For Each item In c.DirtyPropertyArchives
            Dim cname = item.PropertyInfo.LiteralName

            'ハードコーディング処理
            If item.HasSystemValue AndAlso item.SystemValueAttribute.IsHardCode Then
                vsql.AppendDelimiter(", ").AppendFormat("{0} = {1}", cname, item.SystemValueAttribute.HardCode)
                Continue For
            End If

            Dim cval = item.GetLiteralValueForUpdate(c.Instance)
            Dim pname = Me.ParameterPrefix & "V" & vcnt

            '列＝値
            Dim prm = cmd.AddParameter(pname, cval)
            vsql.AppendDelimiter(", ").AppendFormat("{0} = {1}", cname, prm.ParameterName)

            vcnt += 1
        Next

        '条件文字列
        Dim wsql As New StringBuilder
        Dim wcnt As Integer = 0

        For Each item In c.KeyDictionay.Union(From el In c.DirtyPropertyArchives Where el.HasSystemValue AndAlso el.SystemValueAttribute.IsVersionKey Select New KeyValuePair(Of String, LiteralPropertyArchive)(el.PropertyInfo.LiteralName, el))
            Dim keyname = item.Key
            Dim keyval = item.Value.ArchivedValue
            Dim pname = Me.ParameterPrefix & "W" & wcnt

            '抽出条件
            Dim prm = cmd.AddParameter(pname, keyval)
            wsql.AppendDelimiter(" AND ").AppendFormat("{0} = {1}", keyname, prm.ParameterName)

            wcnt += 1
        Next

        '文字列結合
        Dim tname = c.InstanceType.ToEntityDefinition.EntityName
        cmd.CommandText = String.Format("UPDATE {0} SET {1} WHERE {2}", tname, vsql.ToString, wsql.ToString)

        Return cmd
    End Function

    ''' <summary>
    ''' 削除クエリを構築します
    ''' </summary>
    ''' <param name="c"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Private Function BuildDeleteCommand(c As DeleteDirtyObject) As IDbCommand
        Dim cmd = Me.CreateCommand

        '条件文字列
        Dim wsql As New StringBuilder
        Dim wcnt As Integer = 0

        For Each item In c.KeyDictionay
            Dim keyname = item.Key
            Dim keyval = item.Value.ArchivedValue
            Dim pname = Me.ParameterPrefix & "W" & wcnt

            '抽出条件
            Dim prm = cmd.AddParameter(pname, keyval)
            wsql.AppendDelimiter(" AND ").AppendFormat("{0} = {1}", keyname, prm.ParameterName)

            wcnt += 1
        Next

        '文字列結合
        Dim tname = c.InstanceType.ToEntityDefinition.EntityName
        cmd.CommandText = String.Format("DELETE FROM {0} WHERE {1}", tname, wsql.ToString)

        Return cmd
    End Function
#End Region

End Class
